package com.aliyun;

import cn.hutool.core.io.IoUtil;
import cn.hutool.json.JSONUtil;
import com.aliyun.core.emun.AliyunContextType;
import com.aliyun.core.impl.AliyunOssContext;
import com.aliyun.core.impl.AliyunSmsContext;
import com.aliyun.core.pojo.Authority;
import com.aliyun.core.pojo.FileInfo;
import com.aliyun.dysmsapi20170525.Client;
import com.aliyun.dysmsapi20170525.models.SendSmsRequest;
import com.aliyun.dysmsapi20170525.models.SendSmsResponse;
import com.aliyun.handler.AliyunContextHolder;
import com.aliyun.oss.OSS;
import com.aliyun.oss.OSSClientBuilder;
import com.aliyun.oss.model.GetObjectRequest;
import com.aliyun.oss.model.PutObjectRequest;
import com.aliyun.properties.AliyunProperties;
import com.aliyun.properties.pojo.oss.Oss;
import com.aliyun.properties.pojo.sms.Sms;
import com.aliyun.properties.pojo.sms.SmsTemplateParam;
import com.aliyun.properties.pojo.sts.StsToken;
import com.aliyun.teaopenapi.models.Config;
import com.aliyuncs.DefaultAcsClient;
import com.aliyuncs.auth.sts.AssumeRoleRequest;
import com.aliyuncs.auth.sts.AssumeRoleResponse;
import com.aliyuncs.exceptions.ClientException;
import com.aliyuncs.profile.DefaultProfile;
import com.j256.simplemagic.ContentInfo;
import com.j256.simplemagic.ContentInfoUtil;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.codec.digest.DigestUtils;
import org.springframework.beans.factory.InitializingBean;

import javax.annotation.Resource;
import java.io.*;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.time.LocalDateTime;
import java.util.Date;
import java.util.Map;
import java.util.TimeZone;
import java.util.UUID;
import java.util.concurrent.ThreadPoolExecutor;

@Slf4j
public class AliyunTool implements InitializingBean {

    @Resource(type = AliyunProperties.class)
    AliyunProperties properties;
    // Sms服务的临时凭证
    private static Authority smsAuthority;

    private static Authority ossAuthority;

    @Resource
    ThreadPoolExecutor threadPoolExecutor;

    /**
     * 发送短信
     * @param phoneNumber 目标手机号
     * @param templateCode  阿里云短信模板码
     * @param templateParam 短信模板参数
     * @return 短信发送结果
     * @throws Exception 抛出异常
     */
    public String sendSms(String phoneNumber , String templateCode , SmsTemplateParam templateParam) throws Exception {
        // 获取Sms配置信息
        Sms sms = getSms();
        // 创建SmsClient
        Client smsClient = createSmsClient(sms);
        // 构建请求对象
        SendSmsRequest sendSmsRequest = new SendSmsRequest();
        sendSmsRequest.setPhoneNumbers(phoneNumber);
        sendSmsRequest.setSignName(sms.getSignName());
        sendSmsRequest.setTemplateCode(templateCode);
        sendSmsRequest.setTemplateParam(JSONUtil.toJsonStr(templateParam));
        // 发送请求
        SendSmsResponse response = smsClient.sendSms(sendSmsRequest);
        return response.getBody().getMessage();
    }

    /**
     * 上传文件对象
     * @param inputStream 文件流
     * @param originalFilename 文件原始名称
     */
    public FileInfo uploadOss(InputStream inputStream, String originalFilename) throws IOException {
        // 将文件下载到本地
        File loadFile = download(inputStream);
        // 获取Oss配置信息
        Oss oss = getOss();
        OSS ossClient = createOssClient(oss);
        // 获取文件后缀
        String suffix = this.getFileSuffix(originalFilename);
        // 获取文件类型
        String fileContentType = this.getFileContentType(suffix);
        // 根据文件类型获取对应的bucket
        String bucket = this.getBucket(fileContentType);
        // 判断bucket是否存在
        if (!ossClient.doesBucketExist(bucket)){
            // 不存在 则创建bucket
            ossClient.createBucket(bucket);
        }
        // 组合为新的ObjectName
        String fileName = UUID.randomUUID() + suffix;
        String objectName = getDirectory() + fileName;
        // 上传文件
        PutObjectRequest request = new PutObjectRequest(bucket,objectName,loadFile);
        ossClient.putObject(request);
        // 校验文件完整性
        // 创建一个临时文件
        File tempFile = File.createTempFile(UUID.randomUUID().toString(), ".temp");
        // 将上传后的文件保存到 tempFile 文件里
        saveOss(bucket, objectName, tempFile);
        // 原始文件流
        InputStream originalStream = new FileInputStream(loadFile);
        // 目标文件流
        InputStream targetStream = new FileInputStream(tempFile);
        FileInfo fileInfo = new FileInfo();
        String validateMd5 = validateFileIntegrity(originalStream, targetStream);
        if (validateMd5 != null) {
            log.debug("文件校验成功=》上传成功 url: {}", objectName);
            fileInfo.setUrl(objectName);
            // https://BucketName.Endpoint/ObjectName
            fileInfo.setFindUrl("https://" + bucket + "." + oss.getEndpoint() + "/" + objectName);
            fileInfo.setMd5(validateMd5);
            fileInfo.setFileName(fileName);
            fileInfo.setBucket(bucket);
            fileInfo.setContentType(fileContentType);
        } else {
            log.error("文件校验失败=》上传失败 url: {}", objectName);
            this.deleteOss(bucket, objectName);
            fileInfo = null;
        }
        // 5: 删除本地文件
        threadPoolExecutor.execute(() -> {
            tempFile.delete();
            loadFile.delete();
        });
        return fileInfo;
    }


    /**
     * 下载文件到本地 指定路径
     *
     * @param bucket     指定要下载文件的桶
     * @param objectName 指定要下载的文件全路径 不包含桶
     * @param path       指定要下载的文件保存路径
     */
    public void saveOss(String bucket, String objectName, File path) {
        OSS ossClient = createOssClient(getOss());
        GetObjectRequest getObjectRequest = new GetObjectRequest(bucket, objectName);
        ossClient.getObject(getObjectRequest, path);
        log.info("文件下载成功: path: {}", path);
    }

    /**
     * 删除文件
     *
     * @param bucket     指定要删除文件的桶
     * @param objectName 指定要删除的文件全路径 不包含桶
     */
    public void deleteOss(String bucket, String objectName) {
        OSS ossClient = createOssClient(getOss());
        ossClient.deleteObject(bucket, objectName);
        log.warn("删除指定文件成功：bucket:{} object:{}", bucket, objectName);
    }

    /**
     * 当bucket为私有时 生成临时签名URL访问资源
     * @param bucket 桶
     * @param objectName 文件存储路径
     * @param expireHours 签名URL过期小时
     * @return 签名URL
     */
    public String getSignUrlOss(String bucket, String objectName , int expireHours){
        OSS ossClient = createOssClient(getOss());
        return ossClient.generatePresignedUrl(bucket,objectName , new Date(System.currentTimeMillis() + 1000L * 60 * 60 * expireHours)).toString();
    }


    public Sms getSms(){
        return properties.getSms();
    }

    public Oss getOss(){
        return properties.getOss();
    }

    public Client createSmsClient(Sms sms) throws Exception {
        // 尝试获取SmsToken
        AliyunSmsContext context = (AliyunSmsContext) AliyunContextHolder.getContext(AliyunContextType.SMS);
        // sms凭证为空 或者 凭证过期 都需重新获取对应的令牌数据
        if (smsAuthority == null || !new Date().before(smsAuthority.getExpiration())){
            // 请求对应的令牌
            smsAuthority = getStsServiceAuthorization(sms.getSts());
        }
        // 存入上下文对象
        context.setAuthority(smsAuthority);
        Config config = new Config();
        // Sms 服务接入点
        config.setEndpoint(sms.getEndpoint());
        // 设置Sts临时凭证信息
        config.setAccessKeyId(context.getAccessKeyId());
        config.setAccessKeySecret(context.getAccessKeySecret());
        config.setSecurityToken(context.getSecurityToken());
        return new Client(config);
    }

    public OSS createOssClient(Oss oss){
        // 尝试获取SmsToken
        AliyunOssContext context = (AliyunOssContext) AliyunContextHolder.getContext(AliyunContextType.OSS);
        // oss凭证为空 或者 凭证过期 都需重新获取对应的令牌数据
        if (ossAuthority == null || !new Date().before(ossAuthority.getExpiration())){
            // 请求对应的令牌
            ossAuthority = getStsServiceAuthorization(oss.getSts());
        }
        // 存入上下文对象
        context.setAuthority(ossAuthority);
        // 创建 OSSClient 实例
        return new OSSClientBuilder().build(oss.getEndpoint(), context.getAccessKeyId(), context.getAccessKeySecret(), context.getSecurityToken());
    }

    /**
     * 获取Sts Token 临时访问凭证
     */
    private Authority getStsServiceAuthorization(StsToken stsToken){
        // Sts 服务接入点
        String endpoint = stsToken.getEndpoint();
        // RAM角色的资源描述符
        String role_arn = stsToken.getRamRoleArn();
        // RAM用户的AccessKeyId
        String access_key_id = stsToken.getAccessKeyId();
        // RAM用户的AccessKeySecret
        String access_key_secret = stsToken.getAccessKeySecret();
        // 临时访问凭证的有效时间，单位为秒
        Long durationSeconds = stsToken.getDurationSeconds();
        // 发起STS请求所在的地域。建议保留默认值，默认值为空字符串（""）。
        String regionId = "";
        // 自定义角色会话名称，用来区分不同的令牌
        String roleSessionName = UUID.randomUUID().toString();
        // 添加endpoint
        DefaultProfile.addEndpoint(regionId, "Sts", endpoint);
        // 构造default profile
        DefaultProfile profile = DefaultProfile.getProfile(regionId, access_key_id, access_key_secret);
        // 构造client。
        DefaultAcsClient acsClient = new DefaultAcsClient(profile);
        // 获取临时凭证请求对象
        AssumeRoleRequest request = new AssumeRoleRequest();
        request.setRoleArn(role_arn); // 角色的资源描述符
        request.setRoleSessionName(roleSessionName);  // 自定义角色会话名称
        request.setPolicy(null);  // 默认为空，表示不指定临时凭证的权限策略。 临时访问凭证将获得角色拥有的所有权限。
        request.setDurationSeconds(durationSeconds);    // 设置临时访问凭证的有效时间
        // 发起请求 并返回结果
        try {
            AssumeRoleResponse response = acsClient.getAcsResponse(request);
            // 获取临时访问凭证
            AssumeRoleResponse.Credentials credentials = response.getCredentials();
            String expiration = credentials.getExpiration();
            SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'");
            sdf.setTimeZone(TimeZone.getTimeZone("UTC")); // 设置时区为 UTC
            Date date = sdf.parse(expiration);
            return new Authority(credentials.getSecurityToken(), credentials.getAccessKeySecret(), credentials.getAccessKeyId(), date);
        } catch (ClientException e) {
            log.error("获取临时凭证失败 状态码: {} 错误原因：{}",e.getErrCode(),e.getErrMsg());
            throw new RuntimeException(e);
        } catch (ParseException e) {
            log.error("解析日期对象失败 错误原因：{}",e.getMessage());
            throw new RuntimeException(e);
        }
    }

    /**
     * 根据文件类型获取对应的bucket
     *
     * @param contentType 文件类型
     * @return bucket
     */
    public String getBucket(String contentType) {
        // 获取文件类型的前缀
        String prex = contentType.split("/")[0];
        Map<String, String> bucketMap = getOss().getBuckets();
        boolean contained = bucketMap.containsKey(prex);
        if (contained) {
            return bucketMap.get(prex);
        } else {
            return getOss().getDefaultBucket();
        }
    }

    /**
     * 获取当前日期文件夹 用于为文件保存的目录路径
     *
     * @return 文件保存的目录路径
     */
    private static String getDirectory() {
        LocalDateTime localDateTime = LocalDateTime.now();
        // 获取当前年份
        int year = localDateTime.getYear();
        // 获取当前月份
        int month = localDateTime.getMonthValue();
        // 获取当前日期
        int day = localDateTime.getDayOfMonth();
        return year + "/" + month + "/" + day + "/";
    }

    /**
     * 获取文件后缀
     *
     * @param fileName 文件名
     * @return 文件后缀
     */
    public String getFileSuffix(String fileName) {
        if (fileName == null) {
            return "";
        }
        int lastIndexOf = fileName.lastIndexOf(".");
        return fileName.substring(lastIndexOf);
    }

    /**
     * 获取文件类型
     *
     * @param fileSuffix 文件后缀
     * @return 文件类型
     */
    public String getFileContentType(String fileSuffix) {
        if (fileSuffix == null) {
            fileSuffix = "";
        }
        ContentInfo suffixName = ContentInfoUtil.findExtensionMatch(fileSuffix);
        String defaultContentType = "application/octet-stream";   // 默认类型
        if (suffixName != null) {
            defaultContentType = suffixName.getMimeType();
        }
        return defaultContentType;
    }

    /**
     * 校验文件完整性
     *
     * @param originFileMd5  原始文件的MD5值
     * @param targetFilePath 目标文件路径
     * @return true:校验成功 false:校验失败
     */
    public String validateFileIntegrity(String originFileMd5, String targetFilePath) throws IOException {
        InputStream targetFileInputStream = new FileInputStream(targetFilePath);
        try {
            if (originFileMd5.equals(DigestUtils.md5Hex(targetFileInputStream))) {
                return originFileMd5;
            }
            return null;
        } finally {
            IoUtil.close(targetFileInputStream);
        }
    }

    public String validateFileIntegrity(InputStream originStream, InputStream targetStream) throws IOException {
        try {
            String originMd5 = DigestUtils.md5Hex(originStream);
            String targetMd5 = DigestUtils.md5Hex(targetStream);
            if (originMd5.equals(targetMd5)) {
                return originMd5;
            }
            return null;
        } finally {
            IoUtil.close(originStream);
            IoUtil.close(targetStream);
        }
    }

    /**
     * 将文件下载到本地 返回本地文件路径
     *
     * @param inputStream 用户传递的文件流
     * @return 本地文件路径
     */
    private File download(InputStream inputStream) {
        return download(inputStream, null);
    }


    private File download(InputStream inputStream, File file) {
        try {
            if (file == null) {
                file = File.createTempFile(UUID.randomUUID().toString(), ".temp");
            }
            // 将用户传递的文件写出到本地
            OutputStream outputStream = new FileOutputStream(file);
            IoUtil.copy(inputStream, outputStream);
            IoUtil.close(outputStream);
            IoUtil.close(inputStream);
            return file; // 返回文件对象
        } catch (IOException e) {
            log.error("文件下载到本地异常：{}", e.getMessage());
            throw new RuntimeException(e);
        }
    }

    @Override
    public void afterPropertiesSet() throws Exception {
        log.debug("Tool 'AliyunTool' configured for use");
    }
}
